<?php
/**
 * SVN Repository browser
 *
 * @category    RepositoryBrowser
 * @package     application
 * @author      Krzysztof Kardasz <krzysztof@kardasz.eu>
 * @copyright   Copyright (c) 2013 Krzysztof Kardasz
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt  GNU Lesser General Public
 * @version     1.0 $Id: index.php 9 2013-02-10 16:08:11Z krzysztof@kardasz.eu $
 * @link        https://repositorybrowser.googlecode.com
 * @link        http://kardasz.eu
 */

/**
 * Configuration file
 */
$config = require 'config.php';

/**
 * SVN Repository browser class
 *
 * @category    RepositoryBrowser
 * @package     application
 * @author      Krzysztof Kardasz <krzysztof@kardasz.eu>
 * @version     1.0 $Id: index.php 9 2013-02-10 16:08:11Z krzysztof@kardasz.eu $
 */
class RepositoryBrowser {
    /**
     * Configuration params
     */
    const AUTH_ENABLED   = 'auth.enabled';
    const AUTH_USERS     = 'auth.users';
    const REPOSITORY_URL = 'repository.url';
    const REPOSITORY_DIR = 'repository.dir';
    const SVNADMIN_PATH  = 'svnadmin.path';
    const SYSTEM_RM      = 'system.rm';
    const LANGUAGE       = 'language';

    /**
     * Exceptions
     */
    const EX_MSG_NODIR            = 'Config repository.dir is not defined';
    const EX_COD_NODIR            = 5000;
    const EX_MSG_INVALIDDIR       = 'Directory %s not exists';
    const EX_COD_INVALIDDIR       = 5100;
    const EX_MSG_PHPFUNC          = 'PHP function %s not exists or is disabled';
    const EX_COD_PHPFUNC          = 5200;
    const EX_MSG_INVALIDREPO      = 'Repository %s not exists';
    const EX_COD_INVALIDREPO      = 5300;
    const EX_MSG_NORM             = 'Config system.rm is not defined';
    const EX_COD_NORM             = 5400;
    const EX_MSG_INVALIDRM        = 'Program %s not found';
    const EX_COD_INVALIDRM        = 5500;
    const EX_MSG_NOSVNADMIN       = 'Config svnadmin.path is not defined';
    const EX_COD_NOSVNADMIN       = 5600;
    const EX_MSG_INVALIDSVNADMIN  = 'Program %s not found';
    const EX_COD_INVALIDSVNADMIN  = 5700;
    const EX_MSG_NOUSERS          = 'Config auth.users is not defined';
    const EX_COD_NOUSERS          = 5800;
    const EX_MSG_INVALIDUSERS     = 'Config auth.users is not an array';
    const EX_COD_INVALIDUSERS     = 5900;
    const EX_MSG_FAILEDADD        = 'Failed to add %s repository';
    const EX_COD_FAILEDADD        = 6000;
    const EX_MSG_FAILEDDEL        = 'Failed to delete %s repository';
    const EX_COD_FAILEDDEL        = 6100;

    /**
     * Version
     */
    const VERSION = '1.0.0';

    /**
     * Configuration
     *
     * @var array
     */
    protected $config = array();

    /**
     * Translations
     *
     * @var array
     */
    protected $i18n   = null;

    /**
     * Constructor
     *
     * @param array $config configuration params
     */
    public function __construct (array $config) {
        if (isset($config[self::REPOSITORY_DIR])) {
            $config[self::REPOSITORY_DIR] = rtrim($config[self::REPOSITORY_DIR], DIRECTORY_SEPARATOR);
        }
        $this->config = $config;

        if (true === $this->getConfig(self::AUTH_ENABLED, false)) {
            session_name('REPOBROWSERSESSID');
            session_start();
        }
    }

    /**
     * Check for authorization
     *
     * @return boolean
     */
    public function isAuthorized () {
        if (true === $this->getConfig(self::AUTH_ENABLED, false)) {
            return isset($_SESSION['authorized']) && $_SESSION['authorized'] == 1;
        }
        return true;
    }

    /**
     * Authentication
     *
     * @param string $user username
     * @param string $password password
     * @throws Exception
     * @return boolean
     */
    public function authenticate ($user, $password) {
        if (!$this->hasConfig(self::AUTH_USERS)) {
            throw new Exception(
                $this->_(self::EX_MSG_NOUSERS),
                self::EX_COD_NOUSERS
            );
        }
        $users = $this->getConfig(self::AUTH_USERS);
        if (!is_array($users)) {
            throw new Exception(
                $this->_(self::EX_MSG_INVALIDUSERS),
                self::EX_COD_INVALIDUSERS
            );
        }

        if (isset($users[$user]) && $users[$user] === $password) {
            session_regenerate_id(true);
            $_SESSION['authorized'] = 1;
            return true;
        }

        return false;
    }

    /**
     * Signout
     *
     * @return boolean
     */
    public function signout () {
        return session_destroy();
    }

    /**
     * Check is function available
     *
     * @param string $func
     * @return boolean
     */
    public function isFuncAvailable ($func) {
        if (!function_exists($func)) {
            return false;
        }
        $disabled = ini_get('disable_functions');
        if ($disabled) {
            $disabled = explode(',', $disabled);
            $disabled = array_map('trim', $disabled);
            return !in_array($func, $disabled);
        }
        return true;
    }

    /**
     * Check is config param defined
     *
     * @param string $index
     * @return boolean
     */
    public function hasConfig ($index) {
        return !empty($this->config[$index]);
    }

    /**
     * Get config param
     *
     * @param string $index
     * @param mixed $default
     * @return mixed
     */
    public function getConfig ($index, $default = null) {
        return (isset($this->config[$index])) ? $this->config[$index] : $default;
    }

    /**
     * Return repository root dir
     *
     * @throws Exception
     * @return string
     */
    public function getRepositoryDir () {
        if (!$this->hasConfig(self::REPOSITORY_DIR)) {
            throw new Exception(
                $this->_(self::EX_MSG_NODIR),
                self::EX_COD_NODIR
            );
        }

        $dir = $this->getConfig(self::REPOSITORY_DIR);
        if (!is_dir($dir)) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_INVALIDDIR), $dir),
                self::EX_COD_INVALIDDIR
            );
        }
        return $dir;
    }

    /**
     * Return repositories list
     *
     * @return array
     */
    public function getList () {
        $dir = $this->getRepositoryDir();

        $repos = array();
        $list = array_slice(scandir($dir), 2);
        foreach ($list as $file) {
            if (is_dir($dir .  DIRECTORY_SEPARATOR . $file)) {
                $repos[] = $file;
            }
        }
        return $repos;
    }

    /**
     * Create new repository
     *
     * @param string $name
     * @throws Exception
     */
    public function add ($name) {
        if (!$this->hasConfig(self::SVNADMIN_PATH)) {
            throw new Exception(
                $this->_(self::EX_MSG_NOSVNADMIN),
                self::EX_COD_NOSVNADMIN
            );
        }
        $svnadmin = $this->getConfig(self::SVNADMIN_PATH);
        if (!is_executable($svnadmin)) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_INVALIDSVNADMIN), $svnadmin),
                self::EX_COD_INVALIDSVNADMIN
            );
        }
        $path = $this->getRepositoryDir() . DIRECTORY_SEPARATOR . $name;
        $cmd = $svnadmin . ' create ' .escapeshellarg($path);
        $code = $this->exec($cmd);
        if ($code != 0) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_FAILEDADD), $name),
                self::EX_COD_FAILEDADD
            );
        }
    }

    /**
     * Delete repository
     *
     * @param string $name
     * @throws Exception
     */
    public function delete ($name) {
        if (!$this->hasConfig(self::SYSTEM_RM)) {
            throw new Exception(
                $this->_(self::EX_MSG_NORM),
                self::EX_COD_NORM
            );
        }
        $rm = $this->getConfig(self::SYSTEM_RM);
        if (!is_executable($rm)) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_INVALIDRM), $rm),
                self::EX_COD_INVALIDRM
            );
        }
        $path = $this->getRepositoryDir() . DIRECTORY_SEPARATOR . $name;
        $cmd = $rm . ' -rf ' .escapeshellarg($path);
        $code = $this->exec($cmd);
        if ($code != 0) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_FAILEDDEL), $name),
                self::EX_COD_FAILEDDEL
            );
        }
    }

    /**
     * Execute system command
     *
     * @param string $cmd command
     * @throws Exception
     * @return integer status of the executed command (0 means success)
     */
    public function exec ($cmd) {
        if (!$this->isFuncAvailable('system')) {
            throw new Exception(
                sprintf($this->_(self::EX_MSG_PHPFUNC), 'system'),
                self::EX_COD_PHPFUNC
            );
        }
        $code = 0;
        ob_start();
        system($cmd, $code);
        ob_clean();
        return $code;
    }

    /**
     * Look for translation
     *
     * @param string $msg text
     * @return string
     */
    public function _ ($msg) {
        if (null === $this->i18n) {
            $this->i18n = array();
            if ($this->hasConfig(self::LANGUAGE)) {
                $file = 'i18n' . DIRECTORY_SEPARATOR . $this->getConfig(self::LANGUAGE) . '.php';
                if (is_file($file)) {
                    $i18n = require $file;
                    if (is_array($i18n)) {
                        $this->i18n = $i18n;
                    }
                }
            }
        }

        return (isset($this->i18n[$msg])) ? $this->i18n[$msg] : $msg;
    }
}

/**
 * HTTP Request class
 *
 * @category    RepositoryBrowser
 * @package     request
 * @author      Krzysztof Kardasz <krzysztof@kardasz.eu>
 * @version     1.0 $Id: index.php 9 2013-02-10 16:08:11Z krzysztof@kardasz.eu $
 */
class Request {
    /**
     * Check is exists param
     *
     * @param string $name param name
     * @return boolean
     */
    public function hasParam ($name) {
        return isset($_GET[$name]) || isset($_POST[$name]);
    }

    /**
     * Return param
     *
     * @param string $name param name
     * @param mixed $default default value
     * @return mixed
     */
    public function getParam ($name, $default = null) {
        if (isset($_GET[$name])) {
            return $_GET[$name];
        } else if (isset($_POST[$name])) {
            return $_POST[$name];
        }
        return $default;
    }

    /**
     * Return post field
     *
     * @param string $name field name
     * @param mixed $default default value
     * @return mixed
     */
    public function getPost ($name, $default = null) {
        if (isset($_POST[$name])) {
            return $_POST[$name];
        }
        return $default;
    }

    /**
     * Check is request method is post
     * @return boolean
     */
    public function isPost () {
        if (isset($_SERVER['REQUEST_METHOD'])) {
            return 0 === strcasecmp($_SERVER['REQUEST_METHOD'], 'post');
        }
        return false;
    }

    /**
     * Reload current page
     */
    public function reload() {
        header('Location: ' . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
        exit;
    }
}

/* Init objects */
$error = null;
$obj   = new RepositoryBrowser($config);
$req   = new Request();

/* Perform application logic */
try {
    if (!$obj->isAuthorized()) {
        if ($req->isPost() && $req->getPost('action') === 'authenticate') {
            if ($obj->authenticate($req->getPost('login'), $req->getPost('password'))) {
                $req->reload();
            } else {
                $error = $obj->_('Login or password invalid');
            }
        }
    } else {
        if ($req->hasParam('signout')) {
            $obj->signout();
            $req->reload();
        }
        if ($req->hasParam('delete')) {
            $obj->delete($req->getParam('delete'));
            $req->reload();
        }

        if ($req->isPost() && $req->getPost('action') === 'add_repository') {
            $obj->add($req->getPost('repo'));
            $req->reload();
        }
    }
} catch (Exception $e) {
    $error = $e->getMessage();
}

?><!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><?php echo $obj->_('Repository browser');?></title>
    <link rel="stylesheet" href="theme/style.css" type="text/css" />
</head>
<body><div class="wrapper">
    <?php if (!$obj->isAuthorized()) : ?>
        <h1><?php echo $obj->_('Authorization required');?></h1>
        <div class="block">
        <form action="" method="post">
            <p>
                <input type="hidden" name="action" value="authenticate" />
                <label><?php echo $obj->_('Login'); ?>:</label><input type="text" name="login" value="" />
                <label><?php echo $obj->_('Password'); ?>:</label><input type="password" name="password" value="" />
                <a href="#signin"><img src="theme/icons/key_go.png" alt="[signin]" /></a>
            </p>
        </form>
        <?php if ($error) : ?>
        <p class="error">* <?php echo $error; ?></p>
        <?php endif; ?>
        </div>
    <?php else : ?>
    <h1><?php echo $obj->_('Repository browser');?></h1>
    <?php if ($obj->getConfig(RepositoryBrowser::AUTH_ENABLED)) : ?>
    <p class="signout"><a href="?signout"><img src="theme/icons/lock_open.png" alt="[signout]" /> <?php echo $obj->_('signout'); ?></a></p>
    <?php endif; ?>
    <?php
    echo '<table><tr><th><img src="theme/icons/blank.png" alt="[ICO]" /></th><th>' . $obj->_('Name') . '</th><th>' . $obj->_('Url') . '</th><th>' . $obj->_('Delete') . '</th></tr>';
    try {
        foreach ($obj->getList() as $repo) {
            echo vsprintf('<tr><td valign="top"><img src="theme/icons/repo.png" alt="[repo]" /></td><td><a href="%s">%s</a></td><td align="right"><a href="%s">%s</a></td><td align="right"><a href="?delete=%s" title="' . $obj->_('Are you sure to delete %s repository?') . '" class="delete"><img src="theme/icons/delete.png" alt="[delete]" /></a></td></tr>', array(
                $obj->getConfig(RepositoryBrowser::REPOSITORY_URL) . '/'. $repo,
                $repo,
                $obj->getConfig(RepositoryBrowser::REPOSITORY_URL) . '/'. $repo,
                $obj->getConfig(RepositoryBrowser::REPOSITORY_URL) . '/'. $repo,
                $repo,
                $repo
            ));
        }
    } catch (Exception $e) {
        $error = $e->getMessage();
    }
    echo '</table>';
?><div class="block">
    <?php if ($error) : ?>
        <p class="error">* <?php echo $error; ?></p>
    <?php endif; ?>
    <form action="" method="post">
        <p>
            <input type="hidden" name="action" value="add_repository" />
            <label><?php echo $obj->_('Add repository'); ?>:</label><input type="text" name="repo" value="" />
            <a href="#add"><img src="theme/icons/add.png" alt="[add]" /></a>
        </p>
    </form>
</div><?php endif; ?>
</div>
<div class="footer">
	<a href="http://code.google.com/p/repositorybrowser/"><?php echo $obj->_('Repository browser'); ?></a> by <a href="http://www.kardasz.eu">kardasz.eu</a>, Theme by <a href="https://twitter.com/adamwhitcroft">@adamwhitcroft</a>
</div>
<script src="theme/jquery.min.js"></script>
<script>
$('tr:nth-child(2)').addClass('parent');
$('form a').click(function(){$(this).parents('form').get(0).submit(); return false; });
$('a.delete').click(function(){ return confirm($(this).attr('title'));});
</script></body>
</html>